/*
 * Copyright 2013 monkeyboy
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.googlecode.serialization;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.rebind.rpc.BlacklistFilter;
import com.google.gwt.user.rebind.rpc.SerializableTypeOracle;
import com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilder;
import com.google.gwt.user.rebind.rpc.TypeSerializerCreator;
import com.googlecode.gwt.serialization.JsonWriter;

import java.io.PrintWriter;

/**
 * User: monkeyboy
 */
public class JsonWriterGenerator extends Generator {
    private static final String INTERFACE_NAME = JsonWriter.class.getCanonicalName();
    private JClassType rwType;
    private JClassType type;

    @Override
    public String generate(TreeLogger logger, GeneratorContext context, String typeName) throws UnableToCompleteException {

        //logger.log(TreeLogger.Type.WARN, "banana typeName:" + typeName);
        final TypeOracle typeOracle = context.getTypeOracle();
        assert typeOracle != null;
        validateTypes(logger, typeOracle, typeName);
        final String packageName = rwType.getPackage().getName();
        //logger.log(TreeLogger.Type.WARN, "packageName:" + packageName);

        final PropertyOracle propertyOracle = context.getPropertyOracle();

        // Load the blacklist/whitelist
        final BlacklistFilter blacklistTypeFilter = new BlacklistFilter(logger, propertyOracle);

        final SerializableTypeOracleBuilder typesSentFromBrowserBuilder =
                new SerializableTypeOracleBuilder(logger, propertyOracle, context);
        typesSentFromBrowserBuilder.setTypeFilter(blacklistTypeFilter);
        final SerializableTypeOracleBuilder typesSentToBrowserBuilder =
                new SerializableTypeOracleBuilder(logger, propertyOracle, context);
        typesSentToBrowserBuilder.setTypeFilter(blacklistTypeFilter);

        addRoots(logger, typeOracle, typesSentFromBrowserBuilder, typesSentToBrowserBuilder);

        final SerializableTypeOracle typesSentFromBrowser = typesSentFromBrowserBuilder.build(logger);
        final SerializableTypeOracle typesSentToBrowser = typesSentToBrowserBuilder.build(logger);

        final String jsonTypeSerializer = type.getName() + "_JsonTypeSerializer";
        final TypeSerializerCreator tsc =
                new TypeSerializerCreator(logger, typesSentFromBrowser, typesSentToBrowser, context,
                        type.getPackage().getName() + "." + jsonTypeSerializer, jsonTypeSerializer);
        tsc.realize(logger);

        final String jsonWriterName = rwType.getName().replace('.', '_') + "AutogeneratedImpl";
        final PrintWriter jwWriter = context.tryCreate(logger, packageName, jsonWriterName);

        if (jwWriter != null) {
            jwWriter.append("package ").append(packageName).append(";\n");
            jwWriter.append("import com.google.gwt.user.client.rpc.SerializationException;\n");
            jwWriter.append("import com.googlecode.gwt.serialization.JsonSerializationStreamWriter;\n");
            jwWriter.append("import com.googlecode.gwt.serialization.JsonWriter;\n");
            jwWriter.append("import ").append(type.getQualifiedSourceName()).append(";\n");
            jwWriter.append("import ").append(type.getQualifiedSourceName()).append("_JsonTypeSerializer;\n\n");

            jwWriter.append("public class ").append(jsonWriterName).append(" implements ").append(rwType.getQualifiedSourceName()).append(" {\n");
            jwWriter.append("  @Override\n");
            jwWriter.append("  public String write(final ").append(type.getName()).append(" model) {\n");
            jwWriter.append("    final JsonSerializationStreamWriter writer = new JsonSerializationStreamWriter(new ").append(type.getName()).append("_JsonTypeSerializer());\n");
            jwWriter.append("    try {\n");
            jwWriter.append("      writer.writeObject(model);\n");
            jwWriter.append("    } catch (SerializationException e) {\n");
            jwWriter.append("      throw new RuntimeException(e);\n");
            jwWriter.append("    }\n");
            jwWriter.append("    return writer.toString();\n");
            jwWriter.append("  }\n");
            jwWriter.append("}\n");

            context.commit(logger, jwWriter);
        }
        return packageName + "." + jsonWriterName;
    }

    private void validateTypes(
            final TreeLogger logger,
            final TypeOracle typeOracle,
            final String typeName) throws UnableToCompleteException {

        final JClassType interfaceType = typeOracle.findType(INTERFACE_NAME);
        if (interfaceType == null) {
            logger.log(TreeLogger.Type.ERROR, "Unable to find metadata for type " + INTERFACE_NAME);
            throw new UnableToCompleteException();
        }

        rwType = typeOracle.findType(typeName);
        if (rwType == null) {
            logger.log(TreeLogger.Type.ERROR, "Unable to find metadata for type " + typeName);
            throw new UnableToCompleteException();
        }

        if (interfaceType == rwType) {
            logger.log(TreeLogger.Type.ERROR,
                    "You must use a subtype of " + interfaceType.getSimpleSourceName() + " in GWT.create(). E.g.,\n" +
                            "  interface ModelReader extends " + interfaceType.getSimpleSourceName() + "<Model> {}\n" +
                            "  ModelReader reader = GWT.create(ModelReader.class);");
            throw new UnableToCompleteException();
        }

        final JClassType[] implementedInterfaces = rwType.getImplementedInterfaces();
        if (implementedInterfaces.length == 0) {
            logger.log(TreeLogger.Type.ERROR, "No implemented interfaces for " + rwType.getSimpleSourceName());
        }

        // Check type parameter(s)
        for (JClassType t : implementedInterfaces) {
            //logger.log(TreeLogger.Type.WARN, "t:" + t.getQualifiedSourceName());
            //logger.log(TreeLogger.Type.WARN, "interfaceType:" + interfaceType.getQualifiedSourceName());
            if (t.getQualifiedSourceName().equals(interfaceType.getQualifiedSourceName())) {
                final JClassType[] typeArgs = t.isParameterized().getTypeArgs();
                if (typeArgs.length != 1) {
                    logger.log(TreeLogger.Type.WARN, "One type parameter is required for " + t.getName());
                    throw new UnableToCompleteException();
                }
                type = typeArgs[0];
                break;
            }
        }
        if (type == null) {
            logger.log(TreeLogger.Type.WARN, "No type parameter found in " + implementedInterfaces);
            throw new UnableToCompleteException();
        }
        if (type.isParameterized() != null) {
            logger.log(TreeLogger.Type.WARN, "Type parameters for the model are not supported!");
            throw new UnableToCompleteException();
        }
    }

    private void addRoots(
            final TreeLogger logger,
            final TypeOracle typeOracle,
            final SerializableTypeOracleBuilder typesSentFromBrowserBuilder,
            final SerializableTypeOracleBuilder typesSentToBrowserBuilder) throws UnableToCompleteException {
        try {
            addRequiredRoots(logger, typeOracle, typesSentFromBrowserBuilder);
            addRequiredRoots(logger, typeOracle, typesSentToBrowserBuilder);

            typesSentFromBrowserBuilder.addRootType(logger, type);
            typesSentToBrowserBuilder.addRootType(logger, type);
        } catch (NotFoundException e) {
            logger.log(TreeLogger.ERROR, "Unable to find type referenced from remote service", e);
            throw new UnableToCompleteException();
        }
    }

    private static void addRequiredRoots(
            TreeLogger logger,
            final TypeOracle typeOracle,
            final SerializableTypeOracleBuilder stob) throws NotFoundException {
        logger = logger.branch(TreeLogger.DEBUG, "Analyzing implicit types");

        // String is always instantiable.
        final JClassType stringType = typeOracle.getType(String.class.getName());
        stob.addRootType(logger, stringType);

        // IncompatibleRemoteServiceException is always serializable
        final JClassType icseType = typeOracle.getType(IncompatibleRemoteServiceException.class.getName());
        stob.addRootType(logger, icseType);
    }
}
